/*
 * Phys2D - a 2D physics engine based on the work of Erin Catto. The
 * original source remains:
 * 
 * Copyright (c) 2006 Erin Catto http://www.gphysics.com
 * 
 * This source is provided under the terms of the BSD License.
 * 
 * Copyright (c) 2006, Phys2D
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or 
 * without modification, are permitted provided that the following 
 * conditions are met:
 * 
 *  * Redistributions of source code must retain the above 
 *    copyright notice, this list of conditions and the 
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above 
 *    copyright notice, this list of conditions and the following 
 *    disclaimer in the documentation and/or other materials provided 
 *    with the distribution.
 *  * Neither the name of the Phys2D/New Dawn Software nor the names of 
 *    its contributors may be used to endorse or promote products 
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 
 * OF SUCH DAMAGE.
 */
package net.phys2d.raw.test;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferStrategy;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JSplitPane;

import my2Cents.DrawingUtilities;
import net.phys2d.math.MathUtil;
import net.phys2d.math.Matrix2f;
import net.phys2d.math.ROVector2f;
import net.phys2d.math.Vector2f;
import net.phys2d.raw.Arbiter;
import net.phys2d.raw.ArbiterList;
import net.phys2d.raw.BasicJoint;
import net.phys2d.raw.Body;
import net.phys2d.raw.BodyList;
import net.phys2d.raw.Contact;
import net.phys2d.raw.SpringJoint;
import net.phys2d.raw.FixedJoint;
import net.phys2d.raw.Joint;
import net.phys2d.raw.JointList;
import net.phys2d.raw.World;
import net.phys2d.raw.shapes.*;
import net.phys2d.raw.strategies.QuadSpaceStrategy;

/**
 * A common demo box super class.
 * 
 * @author Kevin Glass
 */
public abstract class AbstractDemo {
	/** The frame displaying the demo */
	protected JFrame frame;
	/** The title of the current demo */
	protected String title;
	/** The world containing the physics model */
	protected World world = new World(new Vector2f(0.0f, 10.0f), 10, new QuadSpaceStrategy(20,5));
	/** True if the simulation is running */
	private boolean running = true;
	/** The rendering strategy */
	private BufferStrategy strategy;
	
	
	/** True if we should reset the demo on the next loop */
	protected boolean needsReset;
	/** True if we should render normals */
	private boolean normals = true;
	/** True if we should render contact points */
	private boolean contacts = true;
	protected static boolean GO = true;
	protected Canvas simCanvas;
	protected JMenuBar menubar;
	//private JSplitPane splits;
	
	/**
	 * Create a new demo
	 * 
	 * @param title The title of the demo
	 */
	public AbstractDemo(String title) {
		this.title = title;
	}
	
	
	/** 
	 * Retrieve the title of the demo
	 * 
	 * @return The title of the demo
	 */
	public String getTitle() {
		return title;
	}
	
	/**
	 * Notification that a key was pressed
	 * 
	 * @param c The character of key hit
	 */
	protected void keyHit(char c) {
		if (c == 'r') {
			needsReset = true;
		}
		if (c == 'c') {
			normals = !normals;
			contacts = !contacts;
		}
	}
	
//	protected void wakeupSplitpane() {
//		frame.repaint();
//		splits.invalidate();
//		splits.repaint();
//	}
	
	/**
	 * Initialise the GUI 
	 */
	private void initGUI() {
		frame = new JFrame(title);
		frame.setResizable(true);
		frame.setIgnoreRepaint(true);
		frame.setSize(800,600);
		
		//splits = new JSplitPane();
		
		menubar = new JMenuBar();
		
		int x = (int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() - 800) / 2;
		int y = (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() - 600) / 2;
		
		frame.setLocation(x,y);
		//frame.setLayout(new BorderLayout());
		
		frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				running = false;
				System.exit(0);
			}
		});
		
		simCanvas = new Canvas();
		simCanvas.setIgnoreRepaint(true);
		
		simCanvas.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				keyHit(e.getKeyChar());
			}
			
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == 27) {
					System.exit(0);
				}
			}
		});
		
		addListeners(frame);
		addListeners(simCanvas);
		//addListeners(splits);
		
		//frame.add(splits);
		
		//splits.setTopComponent(menubar);
		//splits.setBottomComponent(simCanvas);
		//splits.setOrientation(JSplitPane.VERTICAL_SPLIT);
		//splits.setEnabled(false);
		
		//frame.add(splits);
		frame.add(simCanvas);
		frame.setJMenuBar(menubar);
		
		simCanvas.setVisible(true);
		menubar.setVisible(true);
		
		//splits.setDividerLocation(25);
		//splits.invalidate();
		
		frame.setVisible(true);
		
		simCanvas.createBufferStrategy(2);
		strategy = simCanvas.getBufferStrategy();
		
		postGui();
		simCanvas.requestFocus();
	}
	
	protected void postGui() {
		
	}
	
	/**
	 * Lets you add listeners right after gui initialization.
	 */
	protected void addListeners(Component c) {
		
	}
	
	/**
	 * Start the simulation running
	 */
	public void start() {
		initGUI();
		initDemo();
		
		float target = 1000 / 60.0f;
		float frameAverage = target;
		long lastFrame = System.currentTimeMillis();
		float yield = 10000f;
		float damping = 0.1f;
		
		long renderTime = 0;
		long logicTime = 0;
		
		int redrawField = 0;
		int redrawWidth = 300;//250;
		
		while (running) {
			// adaptive timing loop from Master Onyx
			long timeNow = System.currentTimeMillis();
			frameAverage = (frameAverage * 10 + (timeNow - lastFrame)) / 11;
			lastFrame = timeNow;
			
			yield+=yield*((target/frameAverage)-1)*damping+0.05f;

			for(int i=0;i<yield;i++) {
				Thread.yield();
			}
			
			// render
			long beforeRender = System.currentTimeMillis();
			Graphics2D g = (Graphics2D) strategy.getDrawGraphics();
			g.setColor(new Color(255,255,255,255));
			g.fillRect(redrawField,0,redrawWidth,simCanvas.getHeight());
			
			if(redrawField + redrawWidth > simCanvas.getWidth()) {
				redrawField = 0;
			} else {
				redrawField += redrawWidth;
			}
			
			draw(g);
			renderGUI(g);
			g.setColor(Color.black);
			//g.drawString("FAv: "+frameAverage,10,50);
			//g.drawString("FPS: "+(int) (1000 / frameAverage),10,70);
			//g.drawString("Yield: "+yield,10,90);
			//g.drawString("Arbiters: "+world.getArbiters().size(),10,110);
			//g.drawString("Bodies: "+world.getBodies().size(),10,130);
			//g.drawString("R: "+renderTime,10,150);
			//g.drawString("L: "+logicTime,10,170);
			//g.drawString("Energy: "+world.getTotalEnergy(),10,190);
			g.drawString("Bodies: "+world.getBodies().size(),15,simCanvas.getHeight() - 55);
			g.setFont(new Font("Arial",0,18));
			if(GO) {
				g.setColor(Color.GREEN);
				g.drawString("-Running-",15,simCanvas.getHeight() - 20);
			} else {
				g.setColor(Color.RED);
				g.drawString("-Paused-",15,simCanvas.getHeight() - 20);
			}
			g.setColor(Color.BLACK);
			g.dispose();
			strategy.show();
			renderTime = System.currentTimeMillis() - beforeRender;
			
			// update data model
			long beforeLogic = System.currentTimeMillis();
			for (int i=0;i<4;i++) {
				if(GO ) {
					try {
						world.step();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
			if(GO) {
				for(int i=0; i<world.getBodies().size(); i++) {
					Body b = world.getBodies().get(i);
					b.think();
				}
			}
			logicTime = System.currentTimeMillis() - beforeLogic;
			
			if (needsReset) {
				world.clear();
				initDemo();
				needsReset = false;
				frameAverage = target;
				yield = 10000f;
			}
			
			update();
		}
	}

	/**
	 * Update the demo - just in case we want to add anything over
	 * the top
	 */
	protected void update() {
	}
	
	/**
	 * Demo customisable GUI render
	 * 
	 * @param g The graphics context to use for rendering here
	 */
	protected void renderGUI(Graphics2D g) {
		g.setColor(Color.black);
		//g.drawString("R - Restart",15,560);
	}
	
	/**
	 * Draw a specific contact point determined from the simulation
	 * 
	 * @param g The graphics context on which to draw
	 * @param contact The contact to draw
	 */
	protected void drawContact(Graphics2D g, Contact contact) {
		int x = (int) contact.getPosition().getX();
		int y = (int) contact.getPosition().getY();
		if (contacts) {
			g.setColor(Color.blue);
			g.fillOval(x-3,y-3,6,6);
		}
		
		if (normals) {
			int dx = (int) (contact.getNormal().getX() * 10);
			int dy = (int) (contact.getNormal().getY() * 10);
			g.setColor(Color.darkGray);
			g.drawLine(x,y,x+dx,y+dy);
		}
	}
	
	/**
	 * Draw a body 
	 * 
	 * @param g The graphics contact on which to draw
	 * @param body The body to be drawn
	 */
	protected void drawBody(Graphics2D g, Body body) {		
		body.draw(g);
	}
	
	/**
	 * Draw a polygon into the demo
	 * 
	 * @param g The graphics to draw the poly onto
	 * @param body The body describing the poly's position
	 * @param poly The poly to be drawn
	 */
	protected void drawPolygonBody(Graphics2D g, Body body, PPolygon poly) {
		g.setColor(Color.black);

		ROVector2f[] verts = poly.getVertices(body.getPosition(), body.getRotation());
		for ( int i = 0, j = verts.length-1; i < verts.length; j = i, i++ ) {			
			g.drawLine(
					(int) (0.5f + verts[i].getX()),
					(int) (0.5f + verts[i].getY()), 
					(int) (0.5f + verts[j].getX()),
					(int) (0.5f + verts[j].getY()));
		}
	}

	/**
	 * Draw a line into the demo
	 * 
	 * @param g The graphics to draw the line onto
	 * @param body The body describing the line's position
	 * @param line The line to be drawn
	 */
	protected void drawLineBody(Graphics2D g, Body body, PLine line) {
//
//		float x = body.getPosition().getX();
//		float y = body.getPosition().getY();
//		float dx = line.getDX();
//		float dy = line.getDY();
//		
		g.setColor(Color.black);
		
		Vector2f[] verts = line.getVertices(body.getPosition(), body.getRotation());
		g.drawLine(
				(int) verts[0].getX(),
				(int) verts[0].getY(), 
				(int) verts[1].getX(),
				(int) verts[1].getY());
		
//		g.setColor(Color.RED);
//		g.drawLine((int) x,(int) y,(int) (x+dx),(int) (y+dy));

	}
	
	/**
	 * Draw a circle in the world
	 * 
	 * @param g The graphics contact on which to draw
	 * @param body The body to be drawn
	 * @param circle The shape to be drawn
	 */
	protected void drawCircleBody(Graphics2D g, Body body, PCircle circle) {
		g.setColor(Color.black);
		float x = body.getPosition().getX();
		float y = body.getPosition().getY();
		float r = circle.getRadius();
		float rot = body.getRotation();
		float xo = (float) (Math.cos(rot) * r);
		float yo = (float) (Math.sin(rot) * r);
		
		g.drawOval((int) (x-r),(int) (y-r),(int) (r*2),(int) (r*2));
		g.drawLine((int) x,(int) y,(int) (x+xo),(int) (y+yo));
	}
	
	/**
	 * Draw a box in the world
	 * 
	 * @param g The graphics contact on which to draw
	 * @param body The body to be drawn
	 * @param box The shape to be drawn
	 */
	protected void drawBoxBody(Graphics2D g, Body body, PBox box) {
		Vector2f[] pts = box.getPoints(body.getPosition(), body.getRotation());
		
		Vector2f v1 = pts[0];
		Vector2f v2 = pts[1];
		Vector2f v3 = pts[2];
		Vector2f v4 = pts[3];
		
		g.setColor(Color.black);
		
		g.drawLine((int) v1.x,(int) v1.y,(int) v2.x,(int) v2.y);
		g.drawLine((int) v2.x,(int) v2.y,(int) v3.x,(int) v3.y);
		g.drawLine((int) v3.x,(int) v3.y,(int) v4.x,(int) v4.y);
		g.drawLine((int) v4.x,(int) v4.y,(int) v1.x,(int) v1.y);
	}
	
	/**
	 * Draw the whole simulation
	 * 
	 * @param g The graphics context on which to draw
	 */
	
	protected void drawBG(Graphics2D g) {
		BodyList bodies = world.getBodies();
		
		for (int i=0;i<bodies.size();i++) {
			Body body = bodies.get(i);
			
			AABox box = body.getShape().getBounds();
			
			int x = (int) box.getOffsetX();
			int y = (int) box.getOffsetY();
			int w = (int) box.getWidth();
			int h = (int) box.getHeight();
			
			x = x + (int) body.getPosition().getX() - w/2;
			y = y + (int) body.getPosition().getY() - h/2;
			
			x = x - (int) body.getVelocity().getX()/10;
			y = y - (int) body.getVelocity().getY()/10;
			
			g.fillRect(x-20,y-20,w+40,h+40);
			
			//g.setColor(Color.red);

			//g.drawRect(x-20, y-20, w+40, h+40);
		}		
	}
	
	protected void draw(Graphics2D g) {
		BodyList bodies = world.getBodies();
		
		Color lastColor = g.getColor();
		
		g.setColor(new Color(255,255,255,255));
		
		drawBG(g);
		
		g.setColor(lastColor);
		
		for (int i=0;i<bodies.size();i++) {
			Body body = bodies.get(i);
			
			drawBody(g, body);
		}
		
		JointList joints = world.getJoints();
		
		for (int i=0;i<joints.size();i++) {
			Joint joint = joints.get(i);
			
			DrawingUtilities.drawJoint(g, joint);
		}
		
		ArbiterList arbs = world.getArbiters();
		
		/*for (int i=0;i<arbs.size();i++) {
			Arbiter arb = arbs.get(i);
			
			Contact[] contacts = arb.getContacts();
			int numContacts = arb.getNumContacts();
			
			for (int j=0;j<numContacts;j++) {
				drawContact(g, contacts[j]);
			}
		}*/
	}
	
	/**
	 * Initialise the demo - clear the world
	 */
	public final void initDemo() {
		world.clear();
		world.setGravity(0,10);
		
		System.out.println("Initialising:" +getTitle());
		init(world);
	}

	/**
	 * Should be implemented by the demo, add the bodies/joints
	 * to the world.
	 * 
	 * @param world The world in which the simulation is going to run
	 */
	protected abstract void init(World world);
	
}
